/**
 * ElementUI 封装的向下广播事件
 *
 * @param componentName 组件名称（需要在 options 上单独定义该字段）
 * @param eventName 事件名称
 * @param params 事件参数
 */
function broadcast(componentName, eventName, params) {
  // 遍历当前组件的所有子组件
  this.$children.forEach((child) => {
    // 获取子组件的名称
    var name = child.$options.componentName

    // 如果子组件的名称与传入的组件名称相同
    if (name === componentName) {
      // 使用 child.$emit 方法触发子组件的事件，并传递事件参数
      child.$emit.apply(child, [eventName].concat(params))
      // 上面为什么使用 concat？https://v2.cn.vuejs.org/v2/api/#vm-emit
      // vm.$emit(eventName, [...args]) 为了兼容 emit 的第二个参数，params 中其实是一组序列
      // params 可能是一个参数也可能是一组参数的数组
    } else {
      // 如果子组件的名称与传入的组件名称不同，则递归调用 broadcast 方法
      // 将事件继续向下广播
      broadcast.apply(child, [componentName, eventName].concat([params]))
      // 这里是为了编码风格的统一使用 broadcast.apply(child, [componentName, eventName, params]) 更加简洁
    }
  })
}

// broadcast 和 dispatch 本质上都是通过遍历
export default {
  methods: {
    /**
     * 向上派发事件
     *
     * @param componentName 组件名称
     * @param eventName 事件名称
     * @param params 事件参数
     */
    dispatch(componentName, eventName, params) {
      var parent = this.$parent || this.$root
      var name = parent.$options.componentName

      // 向上派发事件不需要递归就可以实现，所以这里直接使用 while 循环
      while (parent && (!name || name !== componentName)) {
        // 这里可以理解成是一个链表向前移动的操作
        parent = parent.$parent

        if (parent) {
          name = parent.$options.componentName
        }
      }

      // 遍历结束后，如果找了了对应的组件组件，用 $emit 触发事件
      if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params))
      }
    },

    /**
     * 广播事件
     *
     * 使用当前实例调用 broadcast 方法，将事件名称和参数广播到指定组件名称的所有实例中。
     *
     * @param componentName 组件名称
     * @param eventName 事件名称
     * @param params 事件参数
     */
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params)
    }
  }
}
